<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>rubikMirror</title>
  <style>
      body {
          margin: 0;
          padding: 0;
          overflow: hidden;
      }
  </style>
  <script src="./ex_lib/dat.gui.min.js"></script>
</head>
<body>
<script type="importmap">
  {
      "imports": {
        "three": "./ex_lib/three/three.module.js"
      }
    }
</script>
<script type="module">
  import * as THREE from 'three'
  import {OrbitControls} from './ex_lib/three/jsm/controls/OrbitControls.js'
  import {RubikMirrorCube, DragControl, SceneUtils} from "./bundle_es-obfuscated.js";

  const materials = {};

  const renderer = new THREE.WebGLRenderer({antialias: true});
  renderer.setPixelRatio(window.devicePixelRatio);
  renderer.setSize(window.innerWidth, window.innerHeight);
  // renderer.toneMapping = THREE.ReinhardToneMapping;
  renderer.autoClear = false;
  document.body.appendChild(renderer.domElement);

  const scene = new THREE.Scene();

  const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50);
  camera.position.set(6, 6, 6);
  camera.lookAt(0, 0, 0);

  const cameraControls = new OrbitControls(camera, renderer.domElement);
  cameraControls.minDistance = 1;
  cameraControls.maxDistance = 100;
  // cameraControls.addEventListener( 'change', render );

  scene.add(new THREE.AxesHelper(2000))
  let autoOpLst = [], revertedOpLst, autoOpLstBackUp, autoPlayIndex = 0;
  let pivot = new THREE.Object3D();
  scene.add(pivot);
  let builder = RubikMirrorCube();

  let origin = new THREE.Vector3(0, 0, 0);
  let cameraOppo = new THREE.PerspectiveCamera().copy(camera);

  let camera1 = new THREE.PerspectiveCamera().copy(camera);
  let camera2 = new THREE.PerspectiveCamera().copy(camera);
  let camera3 = new THREE.PerspectiveCamera().copy(camera);
  let SCREEN_WIDTH = window.innerWidth;
  let SCREEN_HEIGHT = window.innerHeight;
  let textureW = SCREEN_WIDTH * 3 / 4, textureH = SCREEN_HEIGHT * 3 / 4;
  const renderTarget = new THREE.WebGLRenderTarget(textureW, textureH, {stencilBuffer: false});

  let helperGeometry = new THREE.PlaneBufferGeometry(textureW, textureH, 1, 1);
  let helperMaterial = new THREE.MeshBasicMaterial({map: renderTarget.texture, side: THREE.BackSide});
  let helperPlane = new THREE.Mesh(helperGeometry, helperMaterial);
  helperPlane.rotation.y = Math.PI;
  let sceneOrtho = new THREE.Scene();
  sceneOrtho.add(helperPlane);

  let aspect = SCREEN_WIDTH / SCREEN_HEIGHT;
  let frustumSize = 11;
  let cameraOtho = new THREE.OrthographicCamera(0.5 * frustumSize * aspect / -1, 0.5 * frustumSize * aspect / 1, frustumSize / 2, frustumSize / -2, 0.1, 40);
  const PuzzleHost = {}
  PuzzleHost.getScene = function () {
    return scene;
  };
  PuzzleHost.isAutoRotating = function () {
    return layerRotating;
  };
  PuzzleHost.getCameraControl = function () {
    return cameraControls;
  };
  PuzzleHost.getIntersects = function (x, y, singleUnit) {
    let vector = new THREE.Vector3((x / window.innerWidth) * 2 - 1, -(y / window.innerHeight) * 2 + 1, 0.5);
    vector = vector.unproject(camera);

    let raycaster = new THREE.Raycaster(camera.position, vector.sub(camera.position).normalize());
    if (singleUnit == null) {
      return raycaster.intersectObjects(raycastingNeeded);
    }
    return raycaster.intersectObjects([singleUnit]);
  };
  PuzzleHost.getPivot = function () {
    return pivot;
  };
  // this.getBoundingBox = function () {
  //     return bounding;
  // };
  PuzzleHost.builder = builder;
  let parts = builder.createParts();
  let raycastingNeeded = builder.getRaycastingNeeded();
  parts.forEach(part => scene.add(part));
  let dragControl = DragControl(PuzzleHost);
  let layerRotating = false, rotatingIndex = 0, rotateStep, rotateParts, rotatingTime, rotateAxis, quaternion;

  function applyRotation(operation) {
    rotateAxis = builder.getRotateAxisBy(operation);
    let rotation = builder.getRotation(operation);
    rotatingTime = Math.round(Math.abs(rotation / Math.PI) * controls.rotatePiDuration);

    rotateParts = builder.getParts(operation);
    if (rotateParts.length === 0) {
      console.log("error, invalid operation");
      return false;
    }
    layerRotating = true;
    rotateStep = rotation / rotatingTime;
    rotatingIndex = 0;
    quaternion = new THREE.Quaternion();
    quaternion.setFromAxisAngle(rotateAxis.normalize(), rotateStep);
    pivot.rotation.set(0, 0, 0);
    pivot.updateMatrixWorld();
    rotateParts.forEach(part => {
      SceneUtils.attach(part, scene, pivot)
    });
    return true;
  }

  document.addEventListener('mousedown', onDocumentMouseDown, false);
  document.addEventListener('mousemove', onDocumentMouseMove, false);
  document.addEventListener('mouseup', onDocumentMouseUp, false);
  function onDocumentMouseDown(event) {
    dragControl.mouseDown(event);
  }

  function onDocumentMouseMove(event) {
    dragControl.mouseMove(event);
  }

  function onDocumentMouseUp(event) {
    dragControl.mouseUp(event);
  }
  function recreatePuzzle(newParts) {
    parts.forEach(part => scene.remove(part));
    parts = newParts;
    parts.forEach(part => scene.add(part));
  }
  function setPuzzleLevel(param) {
    let newParts = builder.setPuzzleLevel(param);
    recreatePuzzle(newParts);
  }

  let lights = [];
  lights[ 0 ] = new THREE.PointLight( 0xffffff, 1, 0 );
  lights[ 1 ] = new THREE.PointLight( 0xffffff, 1, 0 );
  lights[ 2 ] = new THREE.PointLight( 0xffffff, 1, 0 );
  lights[ 3 ] = new THREE.PointLight( 0xffffff, 1, 0 );

  scene.add( lights[ 0 ] );
  scene.add( lights[ 1 ] );
  scene.add( lights[ 2 ] );
  scene.add( lights[ 3 ] );

  let spotLight = new THREE.SpotLight(0xffffff);
  spotLight.position.set(-0, 5, 5);
  spotLight.castShadow = true;
  spotLight.intensity = 0.3;
  scene.add(spotLight);

  let controls = new function () {
    let reverted = false;
    this.randomRotate = function () {
      if (autoOpLst && autoPlayIndex < autoOpLst.length) {
        return;
      }
      reverted = false;
      autoOpLst = builder.getRandomOperations(controls.randomSteps);
      autoPlayIndex = 0;
      autoOpLstBackUp = autoOpLst.slice();
      revertedOpLst = builder.getRevertedOperations(autoOpLst);
    };
    this.switchRandomRotate = function () {
      console.log("-----------switch-----------")
      if (autoPlayIndex < autoOpLst.length) {
        console.log("cannot revert random rotations, random rotating is in progress.")
        return;
      }
      autoPlayIndex = 0;
      if (!reverted) {
        reverted = true;
        autoOpLst = revertedOpLst.slice();
      } else {
        reverted = false;
        autoOpLst = autoOpLstBackUp.slice();
      }
    };
    this.stepBackwards = function () {
      if (!controls.isPaused || layerRotating) {
        console.log("cannot step backward, until rotate stopped");
        return;
      }
      if (autoPlayIndex < 1) {
        console.log("cannot step backward, already reach beginning");
        return;
      }
      autoPlayIndex--;
      let op = autoOpLst[autoPlayIndex];
      let reverted = builder.getRevertOperation(op);
      applyRotation(reverted);
    };
    this.stepForwards = function () {
      if (!controls.isPaused || layerRotating) {
        console.log("cannot step forward, until rotate stopped");
        return;
      }
      if (autoPlayIndex > autoOpLst.length - 1) {
        console.log("cannot step forward, already reach ending");
        return;
      }
      let op = autoOpLst[autoPlayIndex];
      applyRotation(op);
      autoPlayIndex++;
    };
    this.serializeStatus = function () {
      controls.statusCode = builder.serializeStatus();
      console.log(controls.statusCode);
    };
    this.deSerialize = function () {
      if (controls.statusCode && controls.statusCode.length > 30) {
        builder.deSerializeStatus(controls.statusCode);
      }
    };
    this.statusCode = "";
    this.isPaused = false;
    this.rotatePiDuration = 60;
    this.randomSteps = 36;
    this.size = builder.getSize();
    this.gap = builder.getGap();
    this.puzzleLevel = builder.getPuzzleLevel();
    this.extraCameras = ["none", "opposite", "tetrahedron", "backside"];
    this.extraCamera = this.extraCameras[0];
    this.extraCameraType = 0;
    this.numX = builder.getNumX();
    this.numY = builder.getNumY();
    this.numZ = builder.getNumZ();
    this.lessFaces = false;
  };

  let gui = new dat.GUI()
  gui.add(controls, 'randomRotate').name("random rotate");
  gui.add(controls, 'switchRandomRotate').name("switch random rotate");
  gui.add(controls, 'isPaused').name("pause auto rotate");
  gui.add(controls, 'stepBackwards').name("step backward");
  gui.add(controls, 'stepForwards').name("step forward");
  gui.add(controls, 'randomSteps', 10, 300).name("random steps:").step(1).onChange(function (e) {
    controls.randomSteps = e;
  });
  gui.add(controls, 'rotatePiDuration', 10, 150).name("PI duration").step(1).onChange(function (e) {
    controls.rotatePiDuration = e;
  });
  let constructGui = gui.addFolder("Cube structure");
  constructGui.add(controls, 'puzzleLevel', 2, 20).name("hard level:").step(1).onChange(function (e) {
    if (layerRotating) {
      return;
    }
    controls.puzzleLevel = e;
    controls.numX = controls.numY = controls.numZ = e;
    setPuzzleLevel({level: e});
  });
  constructGui.add(controls, 'numX', 2, 22).step(1).listen().onChange(function (e) {
    if (layerRotating) return;
    controls.numX = e;
    setPuzzleLevel({numX: e});
  });
  constructGui.add(controls, 'numY', 1, 22).step(1).listen().onChange(function (e) {
    if (layerRotating) return;
    controls.numY = e;
    setPuzzleLevel({numY: e});
  });
  constructGui.add(controls, 'numZ', 2, 22).step(1).listen().onChange(function (e) {
    if (layerRotating) return;
    controls.numZ = e;
    setPuzzleLevel({numZ: e});
  });
  constructGui.add(controls, 'lessFaces').onChange(function (e) {
    controls.lessFaces = e;
    builder.showLessFaces(e);
  });
  gui.add(controls, 'extraCamera', controls.extraCameras).name("extra camera").onChange(function (e) {
    let index = controls.extraCameras.indexOf(e);
    controls.extraCameraType = index;
  });
  let saveStatusGui = gui.addFolder("Serialize and Deserialize");
  saveStatusGui.add(controls, 'serializeStatus').name("serialize status");
  saveStatusGui.add(controls, 'statusCode').name("serialized:").listen();
  saveStatusGui.add(controls, 'deSerialize').name("deSerialize from text");

  let styleGui = gui.addFolder("Rubik Style");
  styleGui.add(controls, 'size', 1, 6).onChange(function (e) {
    controls.size = e;
    builder.adjustGapAndPartSize({size: e});
  });
  styleGui.add(controls, 'gap', 0, 0.5).onChange(function (e) {
    controls.gap = e;
    builder.adjustGapAndPartSize({gap: e});
  });

  let tetrahedronPoints = [new THREE.Vector3(1, 1, 1), new THREE.Vector3(-1, -1, 1),
      new THREE.Vector3(-1, 1, -1), new THREE.Vector3(1, -1, -1)];
  let tetraAxis = new THREE.Vector3();
  let qua = new THREE.Quaternion();
  function updateLights(t1, t2, t3) {
      lights[0].position.copy(camera.position);
      lights[1].position.copy(t1);
      lights[2].position.copy(t2);
      lights[3].position.copy(t3);
  }
  function onCameraChange() {
      tetraAxis.crossVectors(tetrahedronPoints[0], camera.position).normalize();
      let angle = Math.acos( tetrahedronPoints[0].dot( camera.position ) / tetrahedronPoints[0].length() / camera.position.length() );
      qua.setFromAxisAngle( tetraAxis, angle );
      let scalar = camera.position.length() / tetrahedronPoints[0].length();
      let t1 = tetrahedronPoints[1].clone().applyQuaternion(qua).multiplyScalar(scalar);
      let t2 = tetrahedronPoints[2].clone().applyQuaternion(qua).multiplyScalar(scalar);
      let t3 = tetrahedronPoints[3].clone().applyQuaternion(qua).multiplyScalar(scalar);
      updateLights(t1, t2, t3);

      camera1.position.copy(t1); camera1.lookAt(origin);
      camera2.position.copy(t2); camera2.lookAt(origin);
      camera3.position.copy(t3); camera3.lookAt(origin);

      cameraOppo.position.copy(camera.position);
      cameraOppo.position.negate();
      cameraOppo.lookAt(origin);

      cameraOtho.position.copy(camera.position);
      cameraOtho.lookAt(origin);
  }
  onCameraChange();
  cameraControls.addEventListener("change", function () {
      onCameraChange();
  });
  window.addEventListener( 'resize', function () {
      SCREEN_WIDTH  = window.innerWidth;
      SCREEN_HEIGHT  = window.innerHeight;
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();

      cameraOppo.aspect = camera.aspect;
      cameraOppo.updateProjectionMatrix();
      camera1.aspect = camera.aspect;
      camera1.updateProjectionMatrix();
      camera2.aspect = camera.aspect;
      camera2.updateProjectionMatrix();
      camera3.aspect = camera.aspect;
      camera3.updateProjectionMatrix();

      aspect = camera.aspect;
      cameraOtho.left = 0.5 * frustumSize * aspect / - 2;
      cameraOtho.right = 0.5 * frustumSize * aspect / 2;
      cameraOtho.top = frustumSize / 2;
      cameraOtho.bottom = frustumSize / - 2;
      cameraOtho.updateProjectionMatrix();

      renderer.setSize( window.innerWidth, window.innerHeight );

  }, false )

  function animate() {
    if (!layerRotating && autoOpLst && autoPlayIndex < autoOpLst.length && !controls.isPaused) {
      let autoOp = autoOpLst[autoPlayIndex];
      if (applyRotation(autoOp)) {
        autoPlayIndex++;
      } else {
        autoPlayIndex = autoOpLst.length;
      }
    }
    if (layerRotating) {
      rotatingIndex++;
      pivot.applyQuaternion(quaternion);
      if (rotatingIndex >= rotatingTime) {
        layerRotating = false;
        pivot.updateMatrixWorld();
        rotateParts.forEach(part => {
          SceneUtils.detach(part, pivot, scene);
        });
      }
    }

      requestAnimationFrame(animate);
      dragControl.beforeFrameDraw();
      renderer.clear();
      if (controls.extraCameraType === 1) {
          renderer.setViewport( 0, 0, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
          renderer.render(scene, cameraOppo);
      } else if (controls.extraCameraType === 2) {
          renderer.setViewport( SCREEN_WIDTH/3, 0, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
          renderer.render(scene, camera2);
          renderer.setViewport( 0, SCREEN_HEIGHT*2/3, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
          renderer.render(scene, camera1);
          renderer.setViewport( SCREEN_WIDTH*2/3, SCREEN_HEIGHT*2/3, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
          renderer.render(scene, camera3);
      } else if (controls.extraCameraType === 3) {
          builder.showBackSide();
          renderer.clearDepth();
          renderer.setViewport( SCREEN_WIDTH/3, 0, SCREEN_WIDTH/3, SCREEN_HEIGHT/3);
          // renderer.render(scene, camera);
          renderer.render(scene, cameraOtho);
          builder.showFrontSide();
      }
      renderer.setViewport( 0, 0, SCREEN_WIDTH, SCREEN_HEIGHT);
      renderer.render( scene, camera);
  }

  animate()
</script>
</body>
</html>
